// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: linchuan

#include "coin2/exchange/bithumb/order_v2/parser.h"

#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <fmt/format.h>
#include <rapidjson/document.h>

#include "coin/proto/coin_query.pb.h"
#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::bithumb::order_v2 {

namespace {

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::FillType;
using coin::proto::OrderEvent;
using coin::proto::ProductFillBundle;
using coin::proto::ProductFillElement;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;

using OrderSide = coin::proto::OrderSide;
using OrderContext = base::order::IOrderContext;
using symbology::BithumbCurrency;
using symbology::BithumbProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
using OrderEventType = coin::proto::OrderEvent::OrderEventType;

inline double StringToDouble(const std::string& str) {
  if (str.empty()) {
    return 0;
  }
  auto tmp = boost::replace_all_copy(str, ",", "");
  return stod(tmp);
}

inline bool IsClosedOrder(const std::string& order_status) {
  // order status: 1. Pending 2. Cancel 3. Completed
  if ("Completed" == order_status || "Cancel" == order_status) {
    return true;
  }
  return false;
}
inline OrderSide GetOrderSide(const std::string_view& side) {
  if (side == "bid") {
    return OrderSide::BUY_ORDER;
  } else if (side == "ask") {
    return OrderSide::SELL_ORDER;
  } else {
    return OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

inline bool IsBithumbErrMessage(const JsonValue& doc) {
  if (!doc.HasMember("status")) {
    return true;
  }
  const std::string code = doc["status"].GetString();
  return code != "0000";
}

inline std::string GetBithumbExchangeOrderErrorMsg(const JsonValue& json) {
  if (json.HasMember("message")) {
    if (json["message"].IsObject()) {
      return JsonToString(json["message"]);
    } else {
      return json["message"].GetString();
    }
  }
  return "";
}

inline coin::proto::OrderErrorCode GetOrderErrorCodeFromBithumb(const JsonValue& json) {
  using coin::proto::OrderErrorCode;
  auto result = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
  std::string error_code = json["status"].GetString();
  std::string error_msg = GetBithumbExchangeOrderErrorMsg(json);

  if (error_code == "0000") {
    result = OrderErrorCode::ORDER_NO_ERROR;
    // There is no transaction in progress.
  } else if (error_code == "5600" && error_msg == "거래 진행중인 내역이 존재하지 않습니다.") {
    result = OrderErrorCode::ORDER_NO_ERROR;
  } else if (
      // '대기주문이 없습니다.', there is no waiting order
      error_msg == "\ub300\uae30\uc8fc\ubb38\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.") {
    result = OrderErrorCode::ORDER_NOT_FOUND;
  } else if (
      // '대기주문이 없습니다.', there is no working order
      error_msg == "\ub300\uae30\uc8fc\ubb38\uc774 \uc5c6\uc2b5\ub2c8\ub2e4.") {
    result = OrderErrorCode::ORDER_NOT_FOUND;
  } else {
    LOG(ERROR) << "Unknown error code: " << error_code << " " << error_msg;
  }

  return result;
}

std::string CalcFillId(const JsonValue& fill) {
  static char tmp[1024];
  memset(tmp, 0, sizeof(tmp));
  snprintf(
      tmp,
      sizeof(tmp),
      "%s,%s,%s,%s,%s,%s,%s",
      fill["search"].GetString(),
      fill["order_currency"].GetString(),
      fill["payment_currency"].GetString(),
      fill["transfer_date"].GetString(),
      fill["units"].GetString(),
      fill["fee"].GetString(),
      fill["fee_currency"].GetString());
  std::string_view s(tmp);
  return std::to_string(std::hash<std::string_view>()(s));
}

}  // namespace

void BithumbParser::ParseRestAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance BithumbParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Bithumb");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }
  is_account_balance_initialized_ = true;

  return acc_balance;
}

bool BithumbParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
  {
    "status": "0000",
    "data": {
      "total_krw": "0.000000",
      "in_use_krw": "0.00000000",
      "available_krw": "0.00000000",
      "total_btc": "0.00000000",
      "in_use_btc": "0.00000000",
      "available_btc": "0.00000000",
      "xcoin_last_btc": "13836000",
      ...
    }
  }
  */
  if (IsBithumbErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] " << JsonToString(doc);
    return false;
  }
  if (!doc.HasMember("data") || !doc["data"].IsObject()) {
    LOG(ERROR) << "[ParseBalance] unexpected balance format!" << JsonToString(doc);
    return false;
  }

  std::optional<double> last_total;
  for (const auto& info : doc["data"].GetObject()) {
    std::string name = info.name.GetString();
    const auto& data = info.value.GetString();
    if (name.compare(0, 5, "total") == 0) {
      last_total = stod(data);
      continue;
    }
    if (name.compare(0, 9, "available") == 0 && last_total) {
      CurrencyBalance currency_balance;
      currency_balance.set_currency_native(StringToUpperCopy(name.substr(10)));
      double current_free = stod(data);
      double current_total = *last_total;
      currency_balance.set_available(current_free);
      currency_balance.set_total(current_total);
      currency_balance.set_hold(current_total - current_free);
      raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
    }
  }

  return true;
}

ProductOrderElement BithumbParser::ParseOrderInfo(const JsonValue& data) {
  ProductOrderElement order;
  order.set_market_type(MarketType::Spot);
  order.set_exchange_type(ExchangeType::Bithumb);
  order.set_api_version("v2");
  order.set_exchange_order_id(data["order_id"].GetString());
  std::string symbol = std::string(data["order_currency"].GetString()) + "_" +
                       std::string(data["payment_currency"].GetString());
  order.set_symbol(symbol);
  order.set_price(stod(data["price"].GetString()));
  // order.set_qty(stod(data["units"].GetString()));
  order.set_qty(StringToDouble(data["units"].GetString()));
  order.set_side(GetOrderSide(data["type"].GetString()));
  order.set_timestamp(msg_->timestamp);
  order.set_is_live(true);
  return order;
}

ProductOrderElement
BithumbParser::AddVirtualOrder(std::string symbol, OrderSide side, bool is_live) {
  ProductOrderElement order;
  order.set_market_type(MarketType::Spot);
  order.set_exchange_type(ExchangeType::Bithumb);
  order.set_api_version("v2");
  if (side == OrderSide::BUY_ORDER) {
    order.set_exchange_order_id(symbol + "1");
  } else {
    order.set_exchange_order_id(symbol + "2");
  }
  order.set_symbol(symbol);
  order.set_price(99999999);
  order.set_qty(99999999);
  order.set_side(side);
  order.set_timestamp(msg_->timestamp);
  order.set_tag("permanent");
  if (is_live) {
    order.set_is_live(true);
  } else {
    order.set_cancel_confirmed(true);
  }
  return order;
}

void BithumbParser::ParseRestOpenOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  CHECK(!(msg_->http_context->extra_info.empty()));
  current_native_symbol_ = msg_->http_context->extra_info;
  if (coin2::exchange::bithumb::api_util::RateLimitPreventMode()) {
    *result_.mutable_order_list() = ParseRestOrderInfoListV1(document);
  } else {
    *result_.mutable_order_list() = ParseRestOrderInfoListV2(document);
  }
  if (result_.order_list().each_order().size() > 0) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

ProductOrderBundle BithumbParser::ParseRestOrderInfoListV1(const JsonValue& data) {
  /*
  {
    "status"    : "0000",
    "data"      : [
        {
            "order_currency"    : "BTC",
            "payment_currency"  : "KRW",
            "order_id"          : "C0101000007408440032",
            "order_date"        : "1571728739360570",
            "type"              : "bid",
            "units"             : "5.0",
            "units_remaining"   : "5.0",
            "price"             : "501000",
        }
    ]
  }
  */
  ProductOrderBundle product_order_bundle;
  // first query
  if (!not_first_order_query_[current_native_symbol_]) {
    auto order1 = product_order_bundle.add_each_order();
    auto buy_order1 = AddVirtualOrder(current_native_symbol_, OrderSide::BUY_ORDER, true);
    order1->MergeFrom(buy_order1);
    auto order2 = product_order_bundle.add_each_order();
    auto sell_order1 = AddVirtualOrder(current_native_symbol_, OrderSide::SELL_ORDER, true);
    order2->MergeFrom(sell_order1);
    auto order3 = product_order_bundle.add_each_order();
    auto buy_order2 = AddVirtualOrder(current_native_symbol_, OrderSide::BUY_ORDER, false);
    order3->MergeFrom(buy_order2);
    auto order4 = product_order_bundle.add_each_order();
    auto sell_order2 = AddVirtualOrder(current_native_symbol_, OrderSide::SELL_ORDER, false);
    order4->MergeFrom(sell_order2);
    not_first_order_query_[current_native_symbol_] = true;
  }

  bool is_empty_list = false;
  if (IsBithumbErrMessage(data)) {
    if (GetOrderErrorCodeFromBithumb(data) != OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseRestOrderInfoList] " << JsonToString(data);
    }
    is_empty_list = true;
  }

  if (!is_empty_list && (!data.HasMember("data") || !data["data"].IsArray())) {
    LOG(ERROR) << "Unexpected order list format!" << JsonToString(data);
    return product_order_bundle;
  }

  std::set<std::string> exchange_order_ids_;
  if (!is_empty_list) {
    for (auto& info : data["data"].GetArray()) {
      auto order = product_order_bundle.add_each_order();
      auto new_order = ParseOrderInfo(info);
      CHECK_EQ(current_native_symbol_, new_order.symbol()) << JsonToString(data);
      exchange_order_ids_.emplace(new_order.exchange_order_id());
      order->MergeFrom(new_order);
    }
  }

  for (const auto& oc : ocm_->GetAllProductWorkingOrders(current_native_symbol_)) {
    // Ignore orders that is just submitted.
    if (!oc->is_accepted() || msg_->timestamp - oc->order_accepted_time() < 3e9) {
      continue;
    }
    // Ignore orders found in the open order list.
    if (exchange_order_ids_.count(oc->exchange_order_id()) > 0) {
      seen_in_open_order_list_[oc->proc_order_id()] = true;
      continue;
    }

    // At least seen before then disappeared, then we know
    // this order is dead.
    if (!seen_in_open_order_list_[oc->proc_order_id()]) {
      continue;
    }

    auto& order = *product_order_bundle.add_each_order();
    order.set_market_type(MarketType::Spot);
    order.set_exchange_type(ExchangeType::Bithumb);
    order.set_api_version("v2");
    order.set_exchange_order_id(oc->exchange_order_id());
    order.set_symbol(current_native_symbol_);
    order.set_price(oc->order_price());
    order.set_qty(oc->order_qty());
    order.set_side(oc->order_side());
    order.set_timestamp(msg_->timestamp);
    order.set_is_live(false);
    order.set_cancel_confirmed(true);
  }
  current_native_symbol_.clear();
  return product_order_bundle;
}

ProductOrderBundle BithumbParser::ParseRestOrderInfoListV2(const JsonValue& data) {
  /*
  {
    "status"    : "0000",
    "data"      : [
        {
            "order_currency"    : "BTC",
            "payment_currency"  : "KRW",
            "order_id"          : "C0101000007408440032",
            "order_date"        : "1571728739360570",
            "type"              : "bid",
            "units"             : "5.0",
            "units_remaining"   : "5.0",
            "price"             : "501000",
        }
    ]
  }
  */
  ProductOrderBundle product_order_bundle;

  if (IsBithumbErrMessage(data)) {
    if (GetOrderErrorCodeFromBithumb(data) != OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseRestOrderInfoList] failed, data: " << JsonToString(data);
    }
    return product_order_bundle;
  }
  if (!data.HasMember("data") || !data["data"].IsArray()) {
    LOG(ERROR) << "[ParseRestOrderInfoList] failed, Unexpected order list format!"
               << JsonToString(data);
    return product_order_bundle;
  }
  if (data["data"].Size() == 0) {
    return product_order_bundle;
  }

  for (const auto& order_data : data["data"].GetArray()) {
    const auto& native_symbol = fmt::format(
        "{}_{}",
        order_data["order_currency"].GetString(),
        order_data["payment_currency"].GetString());
    auto& order = *product_order_bundle.add_each_order();
    order.set_market_type(MarketType::Spot);
    order.set_exchange_type(ExchangeType::Bithumb);
    order.set_api_version("v2");
    order.set_exchange_order_id(order_data["order_id"].GetString());
    order.set_symbol(native_symbol);
    order.set_price(stod(order_data["price"].GetString()));
    // order.set_qty(stod(order_data["units"].GetString()));
    order.set_qty(StringToDouble(order_data["units"].GetString()));
    order.set_side(GetOrderSide(order_data["type"].GetString()));
    order.set_timestamp(msg_->timestamp);
    order.set_created_timestamp(std::stoll(order_data["order_date"].GetString()) * 1000'000LL);
    order.set_is_live(true);

    CHECK(order_mgr_) << "order_mgr_ is not set";
    order_mgr_->ProcessOpenOrder(order.exchange_order_id(), order.symbol());
  }

  return product_order_bundle;
}

void BithumbParser::ParseRestFillOrders() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrders] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  current_order_id_ = msg_->http_context->extra_info;
  if (coin2::exchange::bithumb::api_util::RateLimitPreventMode()) {
    *result_.mutable_fill_list() = ParseRestFillInfoListV1(document);
  } else {
    *result_.mutable_fill_list() = ParseRestFillInfoListV2(document);
    *result_.mutable_order_list() = ParseRestOrderInfoListV3(document);
  }
  if (result_.fill_list().each_fill().size() > 0) {
    result_.set_type(MsgType::ORDER_FILL);
  }
  if (result_.order_list().each_order().size() > 0) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

ProductFillBundle BithumbParser::ParseRestFillInfoListV1(const JsonValue& data_arr) {
  /*
  {
    "status"    : "0000",
    "data"      : [
        {
          // search: 0 : All, 1 : Bought, 2 : Sold, 3 : Withdraw in progress
          //         4 : deposit, 5 : withdraw, 9 : deposit KRW in progress
            "search"            : "1",
            "transfer_date"     : 1572252297148997,
            "order_currency"    : "BTC",
            "payment_currency"  : "KRW",
            "units"             : "0.0001",
            "price"             : "10000000",
            "amount"            : "1000",
            "fee_currency"      : "KRW",
            "fee"               : "2.5",
            "order_balance"     : "6.498881591872",
            "payment_balance"   : "1140499718"
        }
    ]
  }
  */
  ProductFillBundle product_fill_bundle;
  if (IsBithumbErrMessage(data_arr)) {
    LOG(ERROR) << "[ParseRestFillInfoList] " << JsonToString(data_arr);
    return product_fill_bundle;
  }

  if (!data_arr.HasMember("data") || !data_arr["data"].IsArray()) {
    LOG(ERROR) << "Unexpected fill list format!" << JsonToString(data_arr);
    return product_fill_bundle;
  }

  if (data_arr["data"].Size() == 0) {
    return product_fill_bundle;
  }

  std::string native_symbol = std::string(data_arr["data"][0]["order_currency"].GetString()) + "_" +
                              std::string(data_arr["data"][0]["payment_currency"].GetString());

  for (auto& data : data_arr["data"].GetArray()) {
    if (std::string_view(data["search"].GetString()) != "1" &&
        std::string_view(data["search"].GetString()) != "2") {
      continue;
    }

    std::string fill_id = CalcFillId(data);
    // If is first query
    if (!not_first_fill_query_[native_symbol]) {
      known_fill_ids_[native_symbol].insert(fill_id);
      continue;
    }

    // ignore the fills long ago
    int64_t fill_timestamp_et = stoll(data["transfer_date"].GetString()) * 1000LL;
    int64_t fill_timestamp_ft = msg_->timestamp;
    if (fill_timestamp_ft - fill_timestamp_et > 100 * 1e9) {
      continue;
    }

    if (known_fill_ids_.count(fill_id) > 0) {
      continue;
    }
    auto fill = product_fill_bundle.add_each_fill();

    std::string symbol = std::string(data["order_currency"].GetString()) + "_" +
                         std::string(data["payment_currency"].GetString());
    fill->set_symbol(symbol);
    fill->set_fill_timestamp(fill_timestamp_ft);
    if (std::string_view(data["search"].GetString()) == "1") {
      fill->set_exchange_order_id(symbol + "1");
    } else {
      fill->set_exchange_order_id(symbol + "2");
    }
    fill->set_price(stod(data["price"].GetString()));
    // fill->set_qty(stod(data["units"].GetString()));
    fill->set_qty(StringToDouble(data["units"].GetString()));
    fill->set_fill_id(fill_id);
    fill->set_fee(stod(data["fee"].GetString()));
    fill->set_fee_currency(data["fee_currency"].GetString());
  }
  not_first_fill_query_[native_symbol] = true;
  return product_fill_bundle;
}

ProductFillBundle BithumbParser::ParseRestFillInfoListV2(const JsonValue& data_arr) {
  /*
  {
    "status": "0000",
    "data": {
      "order_date": "1600409111649615",
      "type": "ask",
      "order_status": "Completed",
      "order_currency": "LINK",
      "payment_currency": "KRW",
      "order_price": "12680",
      "order_qty": "4",
      "cancel_date": "",
      "cancel_type": "",
      "contract": [
        {
          "transaction_date": "1600409145076270",
          "price": "12680",
          "units": "4",
          "fee_currency": "KRW",
          "fee": "15.22",
          "total": "50720"
        }
      ]
    }
  }
  */
  ProductFillBundle product_fill_bundle;
  const auto& order_id = current_order_id_;
  if (order_id.empty()) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed, order id is missing";
    return product_fill_bundle;
  }
  if (IsBithumbErrMessage(data_arr) || !data_arr.HasMember("data")) {
    LOG(ERROR) << "[ParseRestFillInfoList] failed, data: " << JsonToString(data_arr);
    return product_fill_bundle;
  }
  const auto& order_data = data_arr["data"].GetObject();
  const auto& native_symbol = fmt::format(
      "{}_{}",
      order_data["order_currency"].GetString(),
      order_data["payment_currency"].GetString());
  const auto& order_status = order_data["order_status"].GetString();
  if (IsClosedOrder(order_status)) {
    CHECK(order_mgr_) << "order_mgr_ is not set";
    order_mgr_->ProcessClosedOrder(order_id);
  }
  if (!order_data.HasMember("contract") || !order_data["contract"].IsArray() ||
      order_data["contract"].Size() == 0) {
    return product_fill_bundle;
  }
  for (const auto& fill_data : order_data["contract"].GetArray()) {
    auto fill = product_fill_bundle.add_each_fill();
    int64_t fill_timestamp_et = stoll(fill_data["transaction_date"].GetString()) * 1000LL;
    int64_t fill_timestamp_ft = msg_->timestamp;
    fill->set_symbol(native_symbol);
    fill->set_fill_timestamp(fill_timestamp_ft);
    fill->set_exchange_order_id(order_id);
    fill->set_price(stod(fill_data["price"].GetString()));
    // fill->set_qty(stod(fill_data["units"].GetString()));
    fill->set_qty(StringToDouble(fill_data["units"].GetString()));
    std::string fill_id =
        fmt::format("{}_{}_{}_{}", order_id, fill_timestamp_et, fill->price(), fill->qty());
    fill->set_fill_id(fill_id);
    fill->set_fee(stod(fill_data["fee"].GetString()));
    fill->set_fee_currency(fill_data["fee_currency"].GetString());
  }
  return product_fill_bundle;
}

ProductOrderBundle BithumbParser::ParseRestOrderInfoListV3(const JsonValue& data_arr) {
  /*
  {
    "status": "0000",
    "data": {
      "order_date": "1600409111649615",
      "type": "ask",
      "order_status": "Completed",
      "order_currency": "LINK",
      "payment_currency": "KRW",
      "order_price": "12680",
      "order_qty": "4",
      "cancel_date": "",
      "cancel_type": "",
      "contract": [
        {
          "transaction_date": "1600409145076270",
          "price": "12680",
          "units": "4",
          "fee_currency": "KRW",
          "fee": "15.22",
          "total": "50720"
        }
      ]
    }
  }
  */
  ProductOrderBundle product_order_bundle;
  const auto& order_id = current_order_id_;
  if (order_id.empty()) {
    LOG(ERROR) << "[ParseRestOrderInfoListV3] failed, order id is missing";
    return product_order_bundle;
  }
  if (IsBithumbErrMessage(data_arr) || !data_arr.HasMember("data")) {
    LOG(ERROR) << "[ParseRestOrderInfoListV3] failed, data: " << JsonToString(data_arr);
    return product_order_bundle;
  }
  const auto& order_data = data_arr["data"].GetObject();
  const auto& order_status = order_data["order_status"].GetString();
  if (!IsClosedOrder(order_status)) {
    return product_order_bundle;
  }
  const auto& native_symbol = fmt::format(
      "{}_{}",
      order_data["order_currency"].GetString(),
      order_data["payment_currency"].GetString());

  CHECK(order_mgr_) << "order_mgr_ is not set";
  order_mgr_->ProcessClosedOrder(order_id);

  auto* order = product_order_bundle.add_each_order();
  order->set_market_type(MarketType::Spot);
  order->set_exchange_type(ExchangeType::Bithumb);
  order->set_api_version("v2");
  order->set_exchange_order_id(order_id);
  order->set_symbol(native_symbol);
  order->set_price(stod(order_data["order_price"].GetString()));
  order->set_qty(stod(order_data["order_qty"].GetString()));
  order->set_side(GetOrderSide(order_data["type"].GetString()));
  order->set_timestamp(msg_->timestamp);
  order->set_is_live(false);
  order->set_cancel_confirmed(true);
  return product_order_bundle;
}

void BithumbParser::ParseSubmitResponse() {
  /*
  {
    "status"      : "0000",
    "order_id"  : "1428646963419",
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BithumbSubmitOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[BithumbSubmitOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BithumbSubmitOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (IsBithumbErrMessage(json)) {
    auto error_code = GetOrderErrorCodeFromBithumb(json);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetBithumbExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[BithumbSubmitOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  if (!json.HasMember("order_id")) {
    LOG(ERROR) << "[BithumbSubmitOrder] order id not found. " << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_success(false);
    return;
  }
  resp->set_success(true);
  resp->set_exchange_order_id(json["order_id"].GetString());

  // keep order for query fill
  if (!coin2::exchange::bithumb::api_util::RateLimitPreventMode()) {
    CHECK(order_mgr_) << "order_mgr_ is not set";
    order_mgr_->ProcessOpenOrder(resp->exchange_order_id(), resp->symbol());
  }
}

void BithumbParser::ParseCancelResponse() {
  /*
  {
    "status"    : "0000"
  }
  */
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[BithumbCancelOrder] AsioHttpContextStatus Error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  if (http_context->res.result_int() == 429) {
    LOG(ERROR) << "[BithumbCancelOrder] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[BithumbCancelOrder] Parse Error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (IsBithumbErrMessage(json)) {
    auto error_code = GetOrderErrorCodeFromBithumb(json);
    resp->set_error_code(error_code);
    resp->set_exchange_error_msg(GetBithumbExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[BithumbCancelOrder] Error Response" << http_context->res;
    }
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

auto BithumbParser::ParseHeader(const std::shared_ptr<AsioHttpContext>& http_context) const
    -> std::optional<std::string> {
  const auto& header = http_context->res.base();

  int remaining = -1;
  if (header.count("X-RateLimit-Remaining") > 0) {
    remaining = std::stoi(header.at("X-RateLimit-Remaining").data());
  }
  if (remaining < 50) {
    return {};
  }
  int capacity = -1;
  if (header.count("X-RateLimit-Burst-Capacity") > 0) {
    capacity = std::stoi(header.at("X-RateLimit-Burst-Capacity").data());
  }
  if (remaining > capacity) {
    return {};
  }

  nlohmann::json json = {
      {"X-RateLimit-Remaining", remaining},
      {"X-RateLimit-Burst-Capacity", capacity}};
  return json.dump();
}

// Not really used.
void BithumbParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  // websocket unsupported
}

auto BithumbParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg.type);

  try {
    if (msg.type == OrderSubTopicId::WS_MESSAGE) {
      ParseWsMessage();
      return result_;
    }

    if (OrderSubTopicId::REST_ACCOUNT_BALANCE != msg.http_context->query_type) {
      // the rate limit of query balance is different with others
      // expected rate limit window is 1 second. please disable this if rate limit changed
      if (auto rate_limit_info = ParseHeader(msg.http_context)) {
        mb_->PublishEvent("bithumb/req_rate_limit", reinterpret_cast<void*>(&(*rate_limit_info)));
      }
    }

    // REST_MESSAGE
    switch (msg.http_context->query_type) {
      case OrderSubTopicId::SUBMIT_RESPONSE:
        ParseSubmitResponse();
        break;
      case OrderSubTopicId::CANCEL_RESPONSE:
        ParseCancelResponse();
        break;
      case OrderSubTopicId::REST_OPEN_ORDERS:
        ParseRestOpenOrders();
        break;
      case OrderSubTopicId::REST_FILL_ORDERS:
        ParseRestFillOrders();
        break;
      case OrderSubTopicId::REST_ACCOUNT_BALANCE:
        ParseRestAccountBalance();
        break;
      default: {
        LOG(ERROR) << "Parsing Error. " << msg_->DebugString();
        result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
        break;
      }
    }
  } catch (const std::exception& e) {
    LOG(ERROR) << "exception:" << e.what() << " msg:" << msg.DebugString();
    throw;
  } catch (...) {
    LOG(ERROR) << "unknown exception, msg:" << msg.DebugString();
    throw;
  }

  return result_;
}

}  // namespace coin2::exchange::bithumb::order_v2
